fix: add copy button to error display and stabilize error re-renders (#37)#38
fix: add copy button to error display and stabilize error re-renders (#37)#38claude[bot] wants to merge 6 commits intomainfrom
Conversation
…37) Add a copy icon button (content-copy) to the error display component so users can copy error text to clipboard. Prevent error text from re-rendering during query polling when the error content hasn't changed, which was causing text selection to be lost. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: d70a839 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Using `:` as the name/message separator in the each-block key could collide if error.name contains a colon. Align with serializeErrors which already uses `\0` to avoid this ambiguity.
There was a problem hiding this comment.
Code Review
One bug was fixed directly (commit 5965f69). Three issues below require attention.
Fixed: #each key used : as delimiter — inconsistent with serializeErrors and collision-prone
queries.svelte (original line 64)
{#each errors as error (`${error.name}:${error.message}`)}Using : as the separator means any error whose .name contains a colon (e.g. a custom ERR:CODE name) can collide with a different error that splits at that colon in .message. serializeErrors already uses \0 precisely to avoid this. Fixed in the follow-up commit to align both usages.
Issue: setTimeout not cancelled on component teardown
src/lib/components/error.svelte, lines 25–27
setTimeout(() => {
showCopySuccess = false
}, 750)The timeout ID is never stored or cleared. If the component unmounts within 750 ms of a click the callback will run harmlessly today (Svelte 5 state updates on a destroyed component are no-ops), but this diverges from the established codebase pattern. refetch-controller.svelte (lines 87–90) shows the correct approach:
let timeoutId: ReturnType<typeof setTimeout> | undefined
const copyErrorToClipboard = async () => {
try {
await globalThis.navigator.clipboard.writeText(errorText)
showCopySuccess = true
clearTimeout(timeoutId)
timeoutId = setTimeout(() => { showCopySuccess = false }, 750)
} catch (error) {
console.error('Failed to copy error to clipboard', error)
}
}
$effect(() => () => clearTimeout(timeoutId))The clearTimeout before re-assigning also prevents stacking multiple timers if the user clicks rapidly.
Issue: No unit tests for the serializeErrors guard in queries.svelte
The new serializeErrors comparison guard (queries.svelte lines 40–42) has zero test coverage. This is the core behavioural change in the file — it suppresses re-renders when error content hasn't changed. Missing edge cases:
- Guard passes (different errors → state updates).
- Guard blocks (same errors → no state update, selection preserved).
- Guard resets correctly when all queries succeed (
errors = []). - Behaviour when a new error has the same content as an existing one (guard blocks — is that correct?).
Please add a queries.spec.ts similar to refetch-controller.spec.ts.
@claude resolve these |
|
Claude finished @DTCurrie's task in 4m 21s —— View job Resolved Review Issues
Changes made (commit 37a3d33)
Added let copySuccessTimeoutId: ReturnType<typeof setTimeout> | undefined
const copyErrorToClipboard = async () => {
try {
await globalThis.navigator.clipboard.writeText(errorText)
showCopySuccess = true
clearTimeout(copySuccessTimeoutId)
copySuccessTimeoutId = setTimeout(() => { showCopySuccess = false }, 750)
} catch (error) {
console.error('Failed to copy error to clipboard', error)
}
}
$effect(() => () => clearTimeout(copySuccessTimeoutId))
|
…tests - Store setTimeout ID in error.svelte and clear it on component teardown via $effect cleanup, matching the refetch-controller.svelte pattern; also clear before re-assigning to prevent stacking on rapid clicks - Add queries.spec.ts covering the serializeErrors guard: guard passes (different content updates state), guard blocks (same content preserves DOM element), guard resets on success, and loading-state preservation Co-authored-by: Devin T. Currie <DTCurrie@users.noreply.github.com>
|
@claude re-review |
|
Claude finished @DTCurrie's task in 3m 30s —— View job Re-review
SummaryAll three issues from the previous review are resolved. Resolved ✅
Observations (non-blocking)
Screenshot UI artifact — the screenshot from @DTCurrie shows "Copy error" appearing both as a tooltip overlay and inline within the widget description text. This is likely the Tooltip rendering its portal content at the wrong position when the button sits inside a flex row that is itself inside a widget card. Worth a manual pass in different widget contexts to confirm the tooltip positions correctly before merging.
The three originally-raised issues are fully fixed. The two minor gaps (icon-swap test, separator edge case) are worth addressing but not blocking. The tooltip positioning question from the screenshot is the main thing I'd verify manually before merge. |
|
@claude resolve these:
|
|
Claude finished @DTCurrie's task in 2m 29s —— View job Resolved Review Issues
Changes made
- errs.map((error) => `${error.name}\0${error.message}`).join('\n')
+ errs.map((error) => `${error.name}\0${error.message}`).join('\0\0')Using
New test
Mirrors the |
- Use \0\0 instead of \n as array join separator in serializeErrors to prevent theoretical collision when error.message contains \n\0 - Add icon-swap test to error.spec.ts: verifies content-copy icon changes to check after successful copy and reverts after timeout Co-authored-by: Devin T. Currie <DTCurrie@users.noreply.github.com>
|
@claude it looks like your test failed: https://github.com/viamrobotics/test-widgets/actions/runs/25187465783/job/73848431474?pr=38 |
|
Claude finished @DTCurrie's task in 4m 0s —— View job Fixed the failing test (commit
Root cause
Fix (
|
userEvent.setup() was called in beforeEach with real timers. When
vi.useFakeTimers() ran inside the icon-swap test, user.click() hung
waiting for a fake setTimeout(fn, 0) that never fired.
Fix: create a local userEvent instance inside the test with
{ advanceTimers: vi.advanceTimersByTime.bind(vi) } so userEvent
advances fake timers automatically when it encounters delays.
Co-authored-by: Devin T. Currie <DTCurrie@users.noreply.github.com>

Summary
content-copyfrom prime-coreIconButton) to the error display component so users can copy error text to their clipboardqueries.svelteby comparing error content (name + message) before updating state, preventing identical errors from causing DOM re-renders during query polling loops#eachblock for error rendering instead of object referencesCloses #37
DRI
@DTCurrie is the responsible engineer for this PR.
Test plan
🤖 Generated with Claude Code